AWS LambdaとLINE BOT APIで友達になったユーザーをDynamoDBで管理する
こんにちは、せーのです。 先日LINE BOTを作りまして、それ以来私の携帯にDevelopers.ioの新着のお知らせが届いてきているわけですが
一方的に通知されるだけであまりBOTらしい動きをしていないな、ということで、今日は少し実践的に複数のユーザーに対応するAPIを書いてみたいと思います。
仕様を決める
今回やってみるのは
- くらめそちゃんBOTと友達になった人全員にDevelopers.ioの新着をお知らせする
です。
NAT GatewayからNATインスタンスへ
前回も書きましたが、NAT Gatewayはこのソリューションを組むには少々オーバースペックですのでt2.nanoでEC2を一つ立て、それをNATとして使いたいと思います。
NATの立て方はとても簡単です。AWSのMarcketplaceにNAT用のインスタンス、というのがあるのでそれを使ってEC2を立て
送信先/送信元のソースチェックを外し
ルートテーブルで対象となるサブネットからの通信に立てたEC2を指定すればOKです。
前回NAT Gatewayを立てた時に作ったElastic IPをそのままこのEC2につけてやればLINE側の登録を変える必要もありません。
さて、では実装してみましょう。
友達になったユーザーのMIDを管理する
BOTに対して友達になったユーザーがいると、BOTに対してこのようなリクエストが飛びます。
この[opType]が4の場合は友達になった、8の場合はブロックされた、ということを表します。そして飛んできたユーザーのmidを元に
ヘッダ:
- X-Line-ChannelID: Channel ID
- X-Line-ChannelSecret: Channel secret
- X-Line-Trusted-User-With-ACL: Channel MID(BOTのMID。相手ユーザーのではない)
をつけて
TARGET URL: https://trialbot-api.line.me/v1/profiles?mids=[相手ユーザーのMID。複数ある場合はカンマでつなぐ]
に対してGETでリクエストを飛ばすとユーザーの情報が取得できます。
それではユーザーの情報を取得するまでを書いてみましょう。今回は飛んできたリクエストを処理するので前回書いたSignatureによるValidationもキチンと行いましょう。
console.log('Loading function'); var request = require('request'); var crypto = require('crypto'); const CHANNEL_SECRET = 'b142571c3d1daa02ab99ff2f90c7856c'; var getprofileurl = "https://trialbot-api.line.me/v1/profiles"; var receiveOptions = { url: "", headers: { 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true }; var sendOptions = { url: "https://trialbot-api.line.me/v1/events", method: 'POST', headers: { 'Content-Type':'application/json', 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true, body: '' }; exports.handler = (event, context, callback) => { var signature = event.CHANNELSIGNATURE; var eventbody = new Buffer(JSON.stringify(event.body), 'utf8'); var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64'); if (hash != signature){ context.fail("Signature validation failed."); } if (event.body.result[0].content.opType){ if (event.body.result[0].content.opType == 4){ getprofileurl += "?mids=" + event.body.result[0].content.params[0]; console.log("url: " + getprofileurl); receiveOptions.url = getprofileurl; request.get(receiveOptions, function(error, response, body){ if (!error) { console.log(JSON.stringify(body, null, 2)); console.log('send to LINE to get profile.'); context.succeed('done.'); } else { console.log('error: ' + JSON.stringify(error)); } }); } } };
これでBOTに対して友達になるアクションをすると
キチンと動くとprofile取得のリクエストのresponseに
{ "contacts": [ { "displayName": "つよし", "mid": "XXXXXXXXXXXXXXXXXXXXXXXXXX", "pictureUrl": "http://dl.profile.line-cdn.net/XXXXXXXXXXXXXXXXXXXXXXXXXX", "statusMessage": "今年のテーマは「安定」" } ], "count": 1, "display": 1, "pagingRequest": { "start": 1, "display": 1, "sortBy": "MID" }, "start": 1, "total": 1 }
このようなJSONが入ってきます。このmidとdisplayNameをDynamoDBに保管することでユーザーの管理ができます。試しに名前を呼んで返事してみましょう。
console.log('Loading function'); var request = require('request'); var crypto = require('crypto'); const CHANNEL_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'; var getprofileurl = "https://trialbot-api.line.me/v1/profiles"; var receiveOptions = { url: "", headers: { 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true }; var sendOptions = { url: "https://trialbot-api.line.me/v1/events", method: 'POST', headers: { 'Content-Type':'application/json;charset=utf-8', 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true, body: '' }; var senddata={ 'to': [], 'toChannel':1383378250, 'eventType':"138311608800106203", 'content':{ 'contentType': 1, 'toType': 1, 'text': 'くらめそちゃんだよ!' } }; exports.handler = (event, context, callback) => { var signature = event.CHANNELSIGNATURE; var eventbody = new Buffer(JSON.stringify(event.body), 'utf8'); var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64'); if (hash != signature){ context.fail("Signature validation failed."); } if (event.body.result[0].content.opType){ if (event.body.result[0].content.opType == 4){ getprofileurl += "?mids=" + event.body.result[0].content.params[0]; console.log("url: " + getprofileurl); receiveOptions.url = getprofileurl; request.get(receiveOptions, function(error, response, body){ if (!error) { console.log(JSON.stringify(body, null, 2)); console.log('send to LINE to get profile.'); var usermid = body.contacts[0].mid; var username = body.contacts[0].displayName; senddata.to.push(usermid); senddata.content.text = 'くらめそちゃんだよ!' + username + 'さん、これからもよろしくね!'; sendOptions.body = senddata; request.post(sendOptions, function(error, response, body){ if (!error) { console.log(JSON.stringify(response)); console.log(JSON.stringify(body)); console.log('send to LINE.'); context.succeed('done.'); } else { console.log('error: ' + JSON.stringify(error)); } }); } else { console.log('error: ' + JSON.stringify(error)); } }); } } };
DynamoDBを使って管理
ここまで出来れば後はDynamoDBにデータを突っ込んで先日書いたRSS配信のLambda FunctionにDynamoDBからMIDデータを引っ張ってくれば完成です。
まずはDynamoDBのテーブルを一つつくります。hashキーがmidですね。
LambdaにつけていたIAM RoleにDynamoDBの操作権限を追加します。
コードにDynamoDBへのPutを追加します。
console.log('Loading function'); var request = require('request'); var crypto = require('crypto'); const CHANNEL_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'; var getprofileurl = "https://trialbot-api.line.me/v1/profiles"; var receiveOptions = { url: "", headers: { 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true }; var sendOptions = { url: "https://trialbot-api.line.me/v1/events", method: 'POST', headers: { 'Content-Type':'application/json;charset=utf-8', 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true, body: '' }; var senddata={ 'to': [], 'toChannel':1383378250, 'eventType':"138311608800106203", 'content':{ 'contentType': 1, 'toType': 1, 'text': 'くらめそちゃんだよ!' } }; var doc = require('dynamodb-doc'); var dynamo = new doc.DynamoDB(); var dbparams = {}; dbparams.TableName = "linebotusers"; exports.handler = (event, context, callback) => { var signature = event.CHANNELSIGNATURE; var eventbody = new Buffer(JSON.stringify(event.body), 'utf8'); var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64'); if (hash != signature){ context.fail("Signature validation failed."); } if (event.body.result[0].content.opType){ if (event.body.result[0].content.opType == 4){ getprofileurl += "?mids=" + event.body.result[0].content.params[0]; console.log("url: " + getprofileurl); receiveOptions.url = getprofileurl; request.get(receiveOptions, function(error, response, body){ if (!error) { console.log(JSON.stringify(body, null, 2)); console.log('send to LINE to get profile.'); var usermid = body.contacts[0].mid; var username = body.contacts[0].displayName; dbparams.Item = { mid: usermid, name: username }; dynamo.putItem(dbparams, function(err, data) { if (err) { console.log(err, err.stack); } else { console.log('send to DynamoDB.'); console.log(data); senddata.to.push(usermid); senddata.content.text = 'くらめそちゃんだよ!' + username + 'さん、これからもよろしくね!'; sendOptions.body = senddata; request.post(sendOptions, function(error, response, body){ if (!error) { console.log(JSON.stringify(response)); console.log(JSON.stringify(body)); console.log('send to LINE.'); context.succeed('done.'); } else { console.log('error: ' + JSON.stringify(error)); } }); } }); } else { console.log('error: ' + JSON.stringify(error)); } }); } } };
これで友達が追加されたらDynamoDBに値が格納されます。
あとは先日書いたRSSを送信するLambdaFunctionをDynamoDBから取ってくるように書き換えます。
console.log('Loading function'); var FeedParser = require('feedparser'); var request = require('request'); var feed = 'https://dev.classmethod.jp/feed/'; var options = { url: "https://trialbot-api.line.me/v1/events", method: 'POST', headers: { 'Content-Type':'application/json', 'X-Line-ChannelID':'0000000000', 'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX', 'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX' }, json: true, body: '' }; var senddata={ 'to': [], 'toChannel':1383378250, 'eventType':"138311608800106203", 'content':{ 'contentType': 1, 'toType': 1, 'text': '' } }; var doc = require('dynamodb-doc'); var dynamo = new doc.DynamoDB(); var dbparams = {}; dbparams.TableName = "linebotusers"; exports.handler = (event, context, callback) => { var req = request(feed); var feedparser = new FeedParser({}); var items = []; var pubdate = ""; req.on('response', function (res) { this.pipe(feedparser); }); feedparser.on('meta', function(meta) { console.log('==== %s ====', meta.title); }); feedparser.on('readable', function() { while(item = this.read()) { //console.log("item.pubdate: " + item.pubdate + ' ' + item.title); pubdate = item.pubdate.getTime(); now = new Date().getTime(); if (now - pubdate < 900000){ items.push(item); } } }); feedparser.on('end', function() { console.log("article is " + items.length); if (items.length == 0){ context.succeed("No publish articles."); } dynamo.scan(dbparams, function(err, data) { if (err) { console.log(err, err.stack); } else { console.log(data); data.Items.forEach(function(val){ senddata.to.push(val.mid); }); console.log('mids set.'); items.forEach(function(item) { senddata.content.text = '- ' + item.author + 'が書いた、[' + item.title + ']' + '(' + item.link + ')がアップされましたよー☆'; options.body = senddata; console.log('options: ' + JSON.stringify(options)); request.post(options, function(error, response, body){ if (!error) { console.log(JSON.stringify(response)); console.log(JSON.stringify(body)); console.log('send to LINE.'); if( item == items.length - 1 ){ context.succeed('sending function done.'); } } else { console.log('error: ' + JSON.stringify(error)); } }); }); } }); }); };
まとめ
いかがでしたでしょうか。普段APIを触っている方であれば簡単ではないでしょうか。ただ段々処理が増えてコールバック地獄の釜が開いてきているので、次に処理を足すときには一旦整理しないと厳しいですね。